# 本程序用于批量修改群晖LDAP的用户密码，请在users.xlsx文件中填写好账号等信息，就可以完成批量修改
# pip install -i https://pypi.tuna.tsinghua.edu.cn/simple pandas openpyxl ldap3

from ldap3 import Server, Connection, ALL, SUBTREE, MODIFY_REPLACE
from ldap3.core.exceptions import LDAPException, LDAPInvalidCredentialsResult
import hashlib,base64,sys,os
import pandas as pd

# 群晖LDAP服务器配置
SYNOLOGY_LDAP_SERVER = 'ldap://your-synology-ip:389'  # 替换为你的群晖IP地址
SYNOLOGY_LDAPS_SERVER = 'ldaps://your-synology-ip:636'  # 使用LDAPS更安全
SYNOLOGY_BASE_DN = 'dc=example,dc=com'  # 替换为你的群晖LDAP基础DN
ADMIN_DN = 'uid=root,cn=users,,dc=example,dc=com'  # 群晖LDAP管理员账户
ADMIN_PASSWORD = '123456'  # 管理员密码

users = []
old_pass = []
new_pass = []


def readExcelUsers(excelName):
    global users,old_pass,new_pass
    try:
        df = pd.read_excel(excelName,sheet_name=None)
        users = df['账号']["账号"].values.tolist()
        old_pass = df['账号']["旧密码"].values.tolist()
        new_pass = df['账号']["新密码"].values.tolist()
    except Exception as e:
        print(e)


def encode_password_sha(password):
    """将密码编码为{SHA}格式"""
    # 计算SHA1哈希
    sha_hash = hashlib.sha1(password.encode('utf-8')).digest()
    # Base64编码
    encoded_hash = base64.b64encode(sha_hash).decode('utf-8')
    # 格式化为{SHA}格式
    return f'{{SHA}}{encoded_hash}'

def get_user_dn(admin_conn, username, base_dn):
    """根据用户名查找用户的完整DN"""
    try:
        # 尝试多种可能的属性
        search_filter = f'(uid={username})'
        admin_conn.search(search_base=base_dn, 
                         search_filter=search_filter, 
                         search_scope=SUBTREE,
                         attributes=['uid'])
        
        if not admin_conn.entries:
            print(f"未找到用户: {username}")
            return None
        
        user_dn = admin_conn.entries[0].entry_dn
        print(f"找到用户DN: {user_dn}") 
        return user_dn
        
    except LDAPException as e:
        print(f"搜索用户错误: {e}")
        return None

def authenticate_user(username, password, use_ssl=False):
    """验证群晖LDAP用户凭据"""
    conn = None
    try:
        server = Server(SYNOLOGY_LDAP_SERVER, get_info=ALL, use_ssl=use_ssl)
        
        # 首先用管理员账户连接来查找用户DN
        admin_conn = Connection(server, user=ADMIN_DN, password=ADMIN_PASSWORD, auto_bind=True)
        user_dn = get_user_dn(admin_conn, username, SYNOLOGY_BASE_DN)
        admin_conn.unbind()
        
        if not user_dn:
            return False, None
        
        # 尝试使用用户凭据绑定
        conn = Connection(server, user=user_dn, password=password, auto_bind=True)
        print(f"用户 {username} 认证成功")
        return True, conn
            
    except LDAPInvalidCredentialsResult:
        print("无效的凭据")
        if conn:
            conn.unbind()
        return False, None
    except Exception as e:
        print(f"认证错误: {e}")
        if conn:
            conn.unbind()
        return False, None

def change_password_sha_format(admin_conn, user_dn, new_password):
    """使用{SHA}格式修改密码"""
    try:
        # 编码密码为{SHA}格式
        encoded_password = encode_password_sha(new_password)
        print(f"编码后的密码: {encoded_password}")
        
        # 修改密码
        changes = {'userPassword': [(MODIFY_REPLACE, [encoded_password])]}
        
        if admin_conn.modify(user_dn, changes):
            print("密码修改成功（使用{SHA}格式）")
            return True
        else:
            print(f"密码修改失败: {admin_conn.result}")
            return False
            
    except Exception as e:
        print(f"密码修改错误: {e}")
        return False


def get_current_password_hash(admin_conn, user_dn):
    """获取当前密码哈希值（用于调试）"""
    try:
        admin_conn.search(user_dn, '(objectClass=*)', attributes=['userPassword'])
        if admin_conn.entries and 'userPassword' in admin_conn.entries[0]:
            current_hash = admin_conn.entries[0]['userPassword']
            print(f"当前密码哈希: {current_hash}")
            return current_hash
        else:
            print("无法获取当前密码哈希")
            return None
    except Exception as e:
        print(f"获取密码哈希错误: {e}")
        return None

def main():
    global users,old_pass,new_pass
    use_ssl = False
    
    # 建立管理员连接
    try:
        server = Server(SYNOLOGY_LDAP_SERVER, get_info=ALL, use_ssl=use_ssl)
        admin_conn = Connection(server, user=ADMIN_DN, password=ADMIN_PASSWORD, auto_bind=True)
        print("管理员连接成功")
    except Exception as e:
        print(f"管理员连接失败: {e}")
        return
    

    if getattr(sys, 'frozen', False):
        absPath = os.path.dirname(os.path.abspath(sys.executable))
    elif __file__:
        absPath = os.path.dirname(os.path.abspath(__file__))
    excelName = os.path.normpath(absPath +"/users.xlsx")    
    readExcelUsers(excelName)

    user_len = len(users)
    for i in range(user_len):
        username = users[i]
        current_password = old_pass[i]
        new_password = new_pass[i]
        
        print("\n--------------------------")
        # 查找用户DN
        user_dn = get_user_dn(admin_conn, username, SYNOLOGY_BASE_DN)
        if not user_dn:
            continue
        
        # 获取当前密码哈希（调试用）
        print("获取当前密码信息...")
        get_current_password_hash(admin_conn, user_dn)
        
        # 认证用户
        auth_success, user_conn = authenticate_user(username, current_password, use_ssl)
        
        if auth_success:
            print("当前密码验证成功，开始修改密码...")
            
            # 修改密码
            if change_password_sha_format(admin_conn, user_dn, new_password):
                # 验证新密码
                print("验证新密码...")
                auth_success, user_conn = authenticate_user(username, new_password, use_ssl)
                if auth_success:
                    print("密码修改并验证成功!")
                    
                    # 获取修改后的密码哈希
                    print("获取修改后的密码信息...")
                    get_current_password_hash(admin_conn, user_dn)
                else:
                    print("新密码验证失败!")
            else:
                print("密码修改失败!")
            
            if user_conn:
                user_conn.unbind()
        else:
            print("当前密码验证失败，无法修改密码")
    
    admin_conn.unbind()

if __name__ == "__main__":
    main()